package jroguelike.main;

import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import static java.lang.Math.abs;

public final class RLMap {
	public final class FOV {
		public final boolean[] data;

		public boolean visible(int x, int y) {
			return data[xy(x, y)];
		}

		public boolean visible(int xy) {
			return data[xy];
		}

		private FOV() {
			data = new boolean[width * height];
		}
	}

	public String name = "";
	public final int width;
	public final int height;
	private final Terrain[] tiles;
	private final Creature[] creatures;
	private final LinkedList<Creature> creatureList = new LinkedList<>();
	private final RLItem[] items;

	public int range(int xy1, int xy2) {
		return range(x(xy1), y(xy1), x(xy2), y(xy2));
	}

	/**
	 * Returns range from {@code a} to {@code b}. Warning: no checks are made if {@code a} and {@code b} are on same map!
	 */
	public int range(Creature a, Creature b) {
		return range(a.x(), a.y(), b.x(), b.y());
	}

	public static int range(int x1, int y1, int x2, int y2) {
		int dx = Math.abs(x1 - x2);
		int dy = Math.abs(y1 - y2);
		if (dx > dy) return dx + dy / 2;
		return dy + dx / 2;
	}

	private static double min(double a, double b) {
		return (a < b) ? a : b;
	}

	private static double max(double a, double b) {
		return (a > b) ? a : b;
	}

	/**
	 * Checks if point (tx,ty) is visible from (x0,y0) using Bresenham's line algorithm
	 */
	public boolean checkLOS(int x0, int y0, int tx, int ty) {
		if (x0 == tx && y0 == ty) return true;
		if (x0 == tx) {
			int dir = (ty > y0) ? +1 : -1;
			for (int y = y0; y != ty; y += dir) {
				if (!tile(x0, y).passable) return false;
			}
			return true;
		}
		if (y0 == ty) {
			int dir = (tx > x0) ? +1 : -1;
			for (int x = x0; x != tx; x += dir) {
				if (!tile(x, y0).passable) return false;
			}
			return true;
		}
		int deltaX = abs(tx - x0);
		int deltaY = abs(ty - y0);
		int signX = x0 < tx ? 1 : -1;
		int signY = y0 < ty ? 1 : -1;
		int error = deltaX - deltaY;

		while (true) {
			if (x0 == tx && y0 == ty) return true;
			if (!tile(x0, y0).passable) return false;


			int error2 = error * 2;

			if (error2 > -deltaY) {
				error -= deltaY;
				x0 += signX;
			}

			if (error2 < deltaX) {
				error += deltaX;
				y0 += signY;
			}
		}
	}

	public FOV genFOV(int x, int y, int range) {
		FOV fov = new FOV();
		double[] xangles = new double[width * height];
		double[] yangles = new double[width * height];
		boolean[] processed = new boolean[width * height];
		LinkedList<Integer> queue = new LinkedList<Integer>();
		fov.data[xy(x, y)] = true;
		xangles[xy(x, y)] = 2 * range;
		queue.add(xy(x + 1, y));
		queue.add(xy(x - 1, y));
		queue.add(xy(x, y + 1));
		queue.add(xy(x, y - 1));
		while (!queue.isEmpty()) {
			int idx = queue.pop();
			int mx = x(idx);
			int my = y(idx);
			if (!inBounds(mx, my)) continue;
			if (range(mx, my, x, y) > range) continue;
			if (processed[idx]) continue;
			processed[idx] = true;
			int dx = mx - x;
			int dy = my - y;
			int qx = (dx > 0) ? 1 : (dx < 0 ? -1 : 0);
			int qy = (dy > 0) ? 1 : (dy < 0 ? -1 : 0);
			int ax = dx * qx;
			int ay = dy * qy;
			double minangle = Math.atan2(ay, ax + 1);
			double maxangle = Math.atan2(ay + 1, ax);
			double myxa = 2 * range;
			double myya = 0;
			if (qx != 0 && qy != 0) {
				myxa = min(xangles[xy(mx - qx, my)], xangles[xy(mx, my - qy)]);
				myya = max(yangles[xy(mx - qx, my)], yangles[xy(mx, my - qy)]);
			} else if (qx != 0 && qy == 0) {
				myxa = xangles[xy(mx - qx, my)];
				myya = yangles[xy(mx - qx, my)];
			} else if (qx == 0 && qy != 0) {
				myxa = xangles[xy(mx, my - qy)];
				myya = yangles[xy(mx, my - qy)];
			}
			boolean visible;
			if (qx != 0 && qy != 0) {
				visible = (minangle <= myxa) || (maxangle >= myya);
			} else {
				visible = fov.data[xy(mx - qx, my - qy)] && tile(mx - qx, my - qy).passable;
			}
			if (!tile(idx).passable) {
				if (myxa > minangle) myxa = minangle;
				if (myya < maxangle) myya = maxangle;
			}
			xangles[idx] = myxa;
			yangles[idx] = myya;
			fov.data[idx] = visible;
			if (qy == 0) {
				queue.add(xy(mx + qx, my));
				queue.add(xy(mx, my - 1));
				queue.add(xy(mx, my + 1));
			} else if (qx == 0) {
				queue.add(xy(mx, my + qy));
				queue.add(xy(mx - 1, my));
				queue.add(xy(mx + 1, my));
			} else {
				queue.add(xy(mx + qx, my));
				queue.add(xy(mx, my + qy));
			}

		}
		return fov;
	}

	public int xy(int x, int y) {
		return x + y * width;
	}

	public int x(int xy) {
		return xy % width;
	}

	public int y(int xy) {
		return xy / width;
	}

	public boolean inBounds(int x, int y) {
		return (x >= 0 && y >= 0 && x < width && y < height);
	}

	RLMap(int width, int height) {
		this.width = width;
		this.height = height;
		tiles = new Terrain[height * width];
		creatures = new Creature[height * width];
		items = new RLItem[height * width];
		for (int i = 0; i < height * width; i++) tiles[i] = Terrain.FLOOR;
		for (int i = 0; i < height; i++) {
			tiles[i * width] = Terrain.WALL;
			tiles[height - 1 + i * width] = Terrain.WALL;
		}
		for (int i = 0; i < width; i++) {
			tiles[i] = Terrain.WALL;
			tiles[i + (height - 1) * width] = Terrain.WALL;
		}
	}

	public void dropItem(RLItem item, int x, int y) {
		if (item(x, y) == null && tile(x, y).passable) {
			item.moveTo(this, x, y);
			return;
		}
		int r = 1;
		int[] dxs = {-1, -1, 1, 1};
		int[] dys = {-1, 1, -1, 1};
		int[] rxs = {1, 0, 0, -1};
		int[] rys = {0, -1, 1, 0};
		while (true) {
			boolean wasInBounds = false;
			for (int dir = 0; dir < 4; dir++) {
				for (int i = 0; i < r; i++) {
					int tx = x + dxs[dir] * i + rxs[dir] * r;
					int ty = y + dys[dir] * i + rys[dir] * r;
					if (!inBounds(tx, ty)) continue;
					wasInBounds = true;
					if (item(tx, ty) == null && tile(tx, ty).passable) {
						item.moveTo(this, tx, ty);
						return;
					}
				}
			}
			if (!wasInBounds) break;// cannot drop item
			r++;
		}
	}

	public boolean canMoveTo(int x, int y) {
		return inBounds(x, y) && tile(x, y).passable && creature(x, y) == null;
	}

	public Terrain tile(int x, int y) {
		return tiles[x + y * height];
	}

	public Terrain tile(int xy) {
		return tiles[xy];
	}

	public void tile(int x, int y, Terrain newtile) {
		tiles[x + y * height] = newtile;
	}

	public void tile(int xy, Terrain newtile) {
		tiles[xy] = newtile;
	}

	public List<Creature> creatureList() {
		return Collections.unmodifiableList(creatureList);
	}

	public Creature creature(int x, int y) {
		return creatures[x + y * height];
	}

	public Creature creature(int xy) {
		return creatures[xy];
	}

	public RLItem item(int x, int y) {
		return items[xy(x, y)];
	}

	public RLItem item(int xy) {
		return items[xy];
	}

	public void notifyCreatureMovedOut(Creature creature, int x, int y) {
		assert creatures[x + y * height] == creature : "Consistency failure";
		creatures[x + y * height] = null;
		creatureList.remove(creature);
	}

	public void notifyCreatureMovedIn(Creature creature, int x, int y) {
		assert creatures[x + y * height] == null : "Consistency failure";
		creatures[x + y * height] = creature;
		creatureList.add(creature);
	}

	public void notifyCreatureMoved(Creature creature, int x, int y, int tx, int ty) {
		notifyCreatureMovedOut(creature, x, y);
		notifyCreatureMovedIn(creature, tx, ty);
	}

	public void notifyItemMovedOut(RLItem item, int xy) {
		assert items[xy] == item : "Consistency failure";
		items[xy] = null;
	}

	public void notifyItemMovedIn(RLItem item, int xy) {
		assert items[xy] == null : "Consistency failure";
		items[xy] = item;
	}

	public void putBlood(int x, int y) {
		if (!inBounds(x, y)) return;
		if (tile(x, y) == Terrain.FLOOR) tile(x, y, Terrain.B_FLOOR);
		if (tile(x, y) == Terrain.WALL) tile(x, y, Terrain.B_WALL);
	}
}
